---------------Zork Quest--------------
A 4am crack                  2016-09-18
-------------------. updated 2017-12-09
                   |___________________

Name: Zork Quest: Assault on Egreth
  Castle
Genre: adventure
Year: 1988
Authors: Tom Snyder Productions
Publisher: Infocom
Platform: Apple ][+ or later (64K)
Media: 2 single-sided 5.25-inch disks
OS: custom
Previous cracks: The Necromancer/Fc

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  no errors, but copy hangs with the
  drive motor running

Locksmith Fast Disk Backup
  ditto

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  nothing suspicious
  nothing on track $23
  hi-res disk scan shows no usage of
    half-tracks or quarter-tracks

Disk Fixer
  custom bootloader on track $00
  no sign of DOS, ProDOS, Pascal, or
    anything familiar whatsoever

Why didn't any of my copies work?
  An excellent question. Based on the
  lack of any half-tracks or quarter-
  tracks, I'd guess the protection is
  procedural, not structural. i.e.
  There's some code explicitly checking
  whether the disk is original.

Next steps:

  1. Search for the runtime protection
     check (the "easy" way)
  2. If that fails, trace the boot
     until I find it (the "hard" way)
  3. If that fails, I dunno -- go feed
     the ducks or something?

                   ~

               Chapter 1
         In Which We Get Lucky


One thing that all protection checks
have in common is they need to turn on
the drive motor by accessing a specific
address in the $C0xx range. For slot 6,
it's $C0E9, but to allow disks to boot
from any slot, developers usually use
code like this:

  LDX <slot number x 16>
  LDA $C089,X

There's nothing that says you have to
use the X-register as the index or the
accumulator as the load register. But
most RWTS code does, out of convention
I suppose (or fear of messing up such
low-level code in subtle ways).

Also, since developers don't actually
want people finding their protection-
related code, they may try to encrypt
it or obfuscate it on disk, in memory,
or both. But eventually, the code must
exist and the code must run, and it
must run on my machine, and I have the
final say on what my machine does or
does not do.

But sometimes you get lucky.

Turning to my trusty Disk Fixer sector
editor, I search the non-working copy
for "BD 89 C0", which is the opcode
sequence for "LDA $C089,X".

[Disk Fixer]
  ["F"ind]
    ["H"ex]
      ["BD 89 C0"]

                 --v--

------------- DISK SEARCH -------------

$03/$0E-$02   $04/$03-$26


             PRESS [RETURN]

                 --^--

The match on track $04 seems legitimate
(based on the nearly presence of an
"LDA $C08A,X" to select drive 1 or 2 --
something that protection checks rarely
bother to support). But the match on
track $03 looks very... familiar.

; hard-code slot 6 -- very common for
; protection schemes (even if the real
; RWTS on the disk supports booting
; from any slot)
0000:A2 60          LDX   #$60

; turn on the drive
0002:BD 89 C0       LDA   $C089,X

; some sort of Death Counter
0005:A9 56          LDA   #$56
0007:85 F1          STA   $F1
0009:A9 08          LDA   #$08
000B:C6 F0          DEC   $F0
000D:D0 04          BNE   $0013
000F:C6 F1          DEC   $F1

; If the Death Counter hits zero, that
; would be bad. Which part of "Death
; Counter" sounded good to you, anyway?
0011:F0 3C          BEQ   $004F

; look for an #$FB nibble
0013:BC 8C C0       LDY   $C08C,X
0016:10 FB          BPL   $0013
0018:C0 FB          CPY   #$FB
001A:D0 ED          BNE   $0009
001C:F0 00          BEQ   $001E

; kill a few cycles (not pointless,
; because the disk spins independently
; of the CPU, so all of these low-level
; disk reads are highly time-sensitive)
001E:EA             NOP
001F:EA             NOP

; read data latch (note: no BPL loop
; here, we're just reading it once)
0020:BC 8C C0       LDY   $C08C,X

; do a compare to set or clear the
; carry bit (among other things, but
; it's the carry bit we care about)
0023:C0 08          CPY   #$08

; rotate the carry into the low bit of
; the accumulator
0025:2A             ROL

; if we just rolled a "1" bit out of
; the high bit of the accumulator, take
; this branch
0026:B0 0B          BCS   $0033

; next nibble needs to be #$FF
0028:BC 8C C0       LDY   $C08C,X
002B:10 FB          BPL   $0028

; ...otherwise we start over
002D:C0 FF          CPY   #$FF
002F:D0 D8          BNE   $0009

; loop back to get next nibble (this is
; an unconditional branch)
0031:F0 EB          BEQ   $001E

; execution continues here (from the
; "BCS" instruction 6 lines up) --
; get another nibble
0033:BC 8C C0       LDY   $C08C,X
0036:10 FB          BPL   $0033

; stash it in zero page
0038:84 F0          STY   $F0

; if the accumulator is anything but
; the bit pattern %00001010, start over
003A:C9 0A          CMP   #$0A
003C:D0 CB          BNE   $0009

; get one more nibble
003E:BD 8C C0       LDA   $C08C,X
0041:10 FB          BPL   $003E

; more bit twiddling (apparently this
; and the previous nibble combine to
; form a single value, 4-4-encoded)
0043:38             SEC
0044:2A             ROL
0045:25 F0          AND   $F0

; that 4-4-encoded value should be #$FF
0047:49 FF          EOR   #$FF

; branch if it isn't the expected value
0049:D0 04          BNE   $004F

; (success path falls through to here)
; turn off drive motor manually
004B:DD 88 C0       CMP   $C088,X

; and exit gracefully to the caller
004E:60             RTS

; crash
004F:00             BRK

I got lost several times trying to
follow this routine. I think the
easiest way to explain it is to show
the difference between the original
disk and my non-working copy.

                   ~

               Chapter 2
        In Which We Get Visual


Here is the original disk, as seen by
the Copy II+ nibble editor. Nibbles
with extra "0" bits (timing bits) after
them are displayed in inverse on an
original machine, marked here with a
"+" after the nibble.

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK: 00  START: 30EE  LENGTH: 180C

3240: B4 D3 DD 96 B2 FB 9E E7   VIEW
3248: E5 D9 CF B2 D7 D7 F7 F7
3250: F7 F7 B2 AD 97 EA FA BB
3258: A7 BA B7 FA DE AA EB FB
3260: BF FD BB FB+FF FF+FF FF+
3268: FF+FF+FF+D5 AA 96 FF FE
3270: AA AA AA AB FF FF DE AA
3278: EB FF+FF+FF+FF+FF+FF+D5
3280: AA AD FA AE 9D AF B2 9B

---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

It's easy to understand why a simple
sector copy failed. The sequence that
this code is looking for starts at
offset $3263, which is between the end
of one sector and the beginning of the
next. (The data epilogue is at $325C;
the next address prologue is at $326B.)
Sector copiers discard everything
between those delimiters and rebuild
the track with a default pattern of
sync bytes. That pattern doesn't
include an $FB nibble, so the nibble
check fails.

But the EDD bit copy also failed. Here
is the original disk's pattern at
offset $3263:

  - #$FB + timing bit
  - #$FF
  - #$FF + timing bit
  - #$FF
  - #$FF + timing bit

And here is what the same part of the
track looks like on my failed EDD copy:

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK: 00  START: 30EE  LENGTH: 180C

3240: B4 D3 DD 96 B2 FB 9E E7   VIEW
3248: E5 D9 CF B2 D7 D7 F7 F7
3250: F7 F7 B2 AD 97 EA FA BB
3258: A7 BA B7 FA DE AA EB FB
3260: BF FD BB FB+FF FF FF+FF+
3268: FF+FF+FF+D5 AA 96 FF FE
3270: AA AA AA AB FF FF DE AA
3278: EB FF+FF+FF+FF+FF+FF+D5
3280: AA AD FA AE 9D AF B2 9B

---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

A subtle difference! The sequence at
offset $3263 now looks like this:

  - #$FB + timing bit
  - #$FF
  - #$FF
  - #$FF + timing bit
  - #$FF + timing bit

This code is looking for #$FF nibbles
with an alternating pattern of timing
bit, no timing bit, timing bit, no
timing bit. It doesn't find that on the
bit copy, so it knows it's not running
on an original disk.

                   ~

               Chapter 3
  In Which We Think About Automation


Since the release of Passport
https://archive.org/details/Passport4am
I've changed the way I think about
runtime protection checks like this.
I used to think "what's the easiest way
to defeat the code on this disk?" Now I
think "what does the code on this disk
have in common with code I've seen on
other disks, and could I detect and
defeat it automatically?"

I've seen this protection check on many
other disks, but not always in this
form. This check appears to be a self-
contained subroutine. I could probably
just put an "RTS" at the very beginning
of the sector to bypass the check.
[Update: yes, this works.]

But this check isn't always self-
contained. What does it have in common
with other disks? Let's revisit the
code with this question in mind.

According to my records, the last disk
I cracked with this same check was
  #684 Math Shop v1986-10-27

That disk was ProDOS-based, although
the actual magic bit sequence was still
on track 0. But that's not guaranteed;
the caller could put the drive head on
any track before calling the protection
check.

This check starts at offset $00 within
the sector, but there are no absolute
jumps within the code; it's completely
relocatable. On Math Shop, the code
started at offset $9A.

; varies (Math Shop used ProDOS globals
; to determine the boot slot, instead
; of hard-coding it)
0000:A2 60          LDX   #$60
0002:BD 89 C0       LDA   $C089,X

; varies (Math Shop uses different zero
; page addresses)
0005:A9 56          LDA   #$56
0007:85 F1          STA   $F1
0009:A9 08          LDA   #$08
000B:C6 F0          DEC   $F0
000D:D0 04          BNE   $0013
000F:C6 F1          DEC   $F1

; identical (Math Shop starts at a
; different offset, but the length of
; the branch -- #$3C -- is identical)
0011:F0 3C          BEQ   $004F

; identical (including branch lengths)
0013:BC 8C C0       LDY   $C08C,X
0016:10 FB          BPL   $0013
0018:C0 FB          CPY   #$FB
001A:D0 ED          BNE   $0009
001C:F0 00          BEQ   $001E
001E:EA             NOP
001F:EA             NOP
0020:BC 8C C0       LDY   $C08C,X
0023:C0 08          CPY   #$08
0025:2A             ROL
0026:B0 0B          BCS   $0033
0028:BC 8C C0       LDY   $C08C,X
002B:10 FB          BPL   $0028
002D:C0 FF          CPY   #$FF
002F:D0 D8          BNE   $0009
0031:F0 EB          BEQ   $001E
0033:BC 8C C0       LDY   $C08C,X
0036:10 FB          BPL   $0033

; varies (Math Shop uses a different
; zero page address)
0038:84 F0          STY   $F0

; identical (including branch lengths)
003A:C9 0A          CMP   #$0A
003C:D0 CB          BNE   $0009
003E:BD 8C C0       LDA   $C08C,X
0041:10 FB          BPL   $003E
0043:38             SEC
0044:2A             ROL

; varies (address again)
0045:25 F0          AND   $F0

; identical
0047:49 FF          EOR   #$FF

; varies (Math Shop uses a "BEQ" to
; branch to the failure path -- the
; exact opposite logic as this disk!)
0049:D0 04          BNE   $004F

Well, that's upsetting. Right up until
the last line, I was hopeful that I
could automate this. But this disk
demands a 4-4-encoded value of #$FF
immediately after the magic bit
sequence, and Math Shop demands any
value except #$FF -- the exact opposite
condition!

Maybe I can distinguish between them
based on the branch instruction on the
last line? More research is clearly
required.

In the meantime, I have a working patch
for this disk: putting an "RTS" at the
beginning of the protection check.

T03,S0E,$00: A2 -> 60

]PR#6
...works...

To be continued...

Quod erat liberandum.

                   ~

               Changelog


2017-12-09

- fixed minor data corruption in unused
  sector on disk 1 (T1E,S05)

2016-09-18

- initial release

---------------------------------------
A 4am crack                     No. 843
------------------EOF------------------
